﻿import ol_layer_Image from 'ol/layer/Image.js'
import ol_source_ImageCanvas from 'ol/source/ImageCanvas.js'
import {easeOut as ol_easing_easeOut} from 'ol/easing.js'
import ol_style_Style from 'ol/style/Style.js'
import ol_style_Stroke from 'ol/style/Stroke.js'
import ol_style_Fill from 'ol/style/Fill.js'
import {asString as ol_color_asString} from 'ol/color.js'

/** 
 * @classdesc 3D vector layer rendering
 * @constructor
 * @extends {pl.layer.Image}
 * @param {Object} options
 *  @param {ol.layer.Vector} options.source the source to display in 3D
 *  @param {ol.style.Style} options.styler drawing style
 *  @param {number} options.maxResolution  max resolution to render 3D
 *  @param {number} options.defaultHeight default height if none is return by a propertie
 *  @param {function|string|Number} options.height a height function (returns height giving a feature) or a popertie name for the height or a fixed value
 *  @param {Array<number>} options.center center of the view, default [.5,1]
 */
var ol_layer_Vector3D = class ollayerVector3D extends ol_layer_Image {
  constructor(options) {
    options = options || {}

    var canvas = document.createElement('canvas')
    super({
      source: new ol_source_ImageCanvas({
        canvasFunction: function (extent, resolution, pixelRatio, size /*, projection*/) {
          canvas.width = size[0]
          canvas.height = size[1]
          return canvas
        }
      }),
      center: options.center || [.5, 1],
      defaultHeight: options.defaultHeight || 0,
      maxResolution: options.maxResolution || Infinity
    })

    this._source = options.source
    this.height_ = this.getHfn(options.height)

    this.setStyle(options.style)

    this.on(['postcompose', 'postrender'], this.onPostcompose_.bind(this))
  }
  /**
   * Set the height function for the layer
   * @param {function|string|Number} height a height function (returns height giving a feature) or a popertie name or a fixed value
   */
  setHeight(height) {
    this.height_ = this.getHfn(height)
    this.changed()
  }
  /**
   * Set style associated with the layer
   * @param {ol.style.Style} s
   */
  setStyle(s) {
    if (s instanceof ol_style_Style)
      this._style = s
    else
      this._style = new ol_style_Style()
    if (!this._style.getStroke()) {
      this._style.setStroke(new ol_style_Stroke({
        width: 1,
        color: 'red'
      }))
    }
    if (!this._style.getFill()) {
      this._style.setFill(new ol_style_Fill({ color: 'rgba(0,0,255,0.5)' }))
    }
    if (!this._style.getText()) {
      this._style.setText(new ol_style_Fill({
        color: 'red'
      })
      )
    }
    // Get the geometry
    if (s && s.getGeometry()) {
      var geom = s.getGeometry()
      if (typeof (geom) === 'function') {
        this.set('geometry', geom)
      } else {
        this.set('geometry', function () { return geom })
      }
    } else {
      this.set('geometry', function (f) { return f.getGeometry() })
    }
  }
  /**
   * Get style associated with the layer
   * @return {ol.style.Style}
   */
  getStyle() {
    return this._style
  }
  /** Calculate 3D at potcompose
   * @private
   */
  onPostcompose_(e) {
    var res = e.frameState.viewState.resolution
    if (res > this.get('maxResolution'))
      return
    this.res_ = res * 400

    if (this.animate_) {
      var elapsed = e.frameState.time - this.animate_
      if (elapsed < this.animateDuration_) {
        this.elapsedRatio_ = this.easing_(elapsed / this.animateDuration_)
        // tell OL3 to continue postcompose animation
        e.frameState.animate = true
      } else {
        this.animate_ = false
        this.height_ = this.toHeight_
      }
    }

    var ratio = e.frameState.pixelRatio
    var ctx = e.context
    var m = this.matrix_ = e.frameState.coordinateToPixelTransform
    // Old version (matrix)
    if (!m) {
      m = e.frameState.coordinateToPixelMatrix,
        m[2] = m[4]
      m[3] = m[5]
      m[4] = m[12]
      m[5] = m[13]
    }
    this.center_ = [
      ctx.canvas.width * this.get('center')[0] / ratio,
      ctx.canvas.height * this.get('center')[1] / ratio
    ]

    var f = this._source.getFeaturesInExtent(e.frameState.extent)

    ctx.save()
    ctx.scale(ratio, ratio)
    var s = this.getStyle()
    ctx.lineWidth = s.getStroke().getWidth()
    ctx.lineCap = s.getStroke().getLineCap()
    ctx.strokeStyle = ol_color_asString(s.getStroke().getColor())
    ctx.fillStyle = ol_color_asString(s.getFill().getColor())
    var builds = []
    for (var i = 0; i < f.length; i++) {
      builds.push(this.getFeature3D_(f[i], this._getFeatureHeight(f[i])))
    }
    this.drawFeature3D_(ctx, builds)
    ctx.restore()
  }
  /** Create a function that return height of a feature
   * @param {function|string|number} h a height function or a popertie name or a fixed value
   * @return {function} function(f) return height of the feature f
   * @private
   */
  getHfn(h) {
    switch (typeof (h)) {
      case 'function': return h
      case 'string': {
        var dh = this.get('defaultHeight')
        return (function (f) {
          return (Number(f.get(h)) || dh)
        })
      }
      case 'number': return (function ( /*f*/) { return h })
      default: return (function ( /*f*/) { return 10 })
    }
  }
  /** Animate rendering
   * @param {*} options
   *  @param {string|function|number} options.height an attribute name or a function returning height of a feature or a fixed value
   *  @param {number} options.duration the duration of the animatioin ms, default 1000
   *  @param {ol.easing} options.easing an ol easing function
   *	@api
   */
  animate(options) {
    options = options || {}
    this.toHeight_ = this.getHfn(options.height)
    this.animate_ = new Date().getTime()
    this.animateDuration_ = options.duration || 1000
    this.easing_ = options.easing || ol_easing_easeOut
    // Force redraw
    this.changed()
  }
  /** Check if animation is on
  *	@return {bool}
  */
  animating() {
    if (this.animate_ && new Date().getTime() - this.animate_ > this.animateDuration_) {
      this.animate_ = false
    }
    return !!this.animate_
  }
  /** Get height for a feature
   * @param {ol.Feature} f
   * @return {number}
   * @private
   */
  _getFeatureHeight(f) {
    if (this.animate_) {
      var h1 = this.height_(f)
      var h2 = this.toHeight_(f)
      return (h1 * (1 - this.elapsedRatio_) + this.elapsedRatio_ * h2)
    }
    else
      return this.height_(f)
  }
  /** Get hvector for a point
   * @private
   */
  hvector_(pt, h) {
    var p0 = [
      pt[0] * this.matrix_[0] + pt[1] * this.matrix_[1] + this.matrix_[4],
      pt[0] * this.matrix_[2] + pt[1] * this.matrix_[3] + this.matrix_[5]
    ]
    return {
      p0: p0,
      p1: [
        p0[0] + h / this.res_ * (p0[0] - this.center_[0]),
        p0[1] + h / this.res_ * (p0[1] - this.center_[1])
      ]
    }
  }
  /** Get a vector 3D for a feature
   * @private
   */
  getFeature3D_(f, h) {
    var geom = this.get('geometry')(f)
    var c = geom.getCoordinates()
    switch (geom.getType()) {
      case "Polygon":
        c = [c]
      // fallthrough
      case "MultiPolygon":
        var build = []
        for (var i = 0; i < c.length; i++) {
          for (var j = 0; j < c[i].length; j++) {
            var b = []
            for (var k = 0; k < c[i][j].length; k++) {
              b.push(this.hvector_(c[i][j][k], h))
            }
            build.push(b)
          }
        }
        return { type: "MultiPolygon", feature: f, geom: build }
      case "Point":
        return { type: "Point", feature: f, geom: this.hvector_(c, h) }
      default: return {}
    }
  }
  /** Draw 3D feature
   * @private
   */
  drawFeature3D_(ctx, build) {
    var i, j, b, k
    // Construct
    for (i = 0; i < build.length; i++) {
      switch (build[i].type) {
        case "MultiPolygon": {
          for (j = 0; j < build[i].geom.length; j++) {
            b = build[i].geom[j]
            for (k = 0; k < b.length; k++) {
              ctx.beginPath()
              ctx.moveTo(b[k].p0[0], b[k].p0[1])
              ctx.lineTo(b[k].p1[0], b[k].p1[1])
              ctx.stroke()
            }
          }
          break
        }
        case "Point": {
          var g = build[i].geom
          ctx.beginPath()
          ctx.moveTo(g.p0[0], g.p0[1])
          ctx.lineTo(g.p1[0], g.p1[1])
          ctx.stroke()
          break
        }
        default: break
      }
    }
    // Roof
    for (i = 0; i < build.length; i++) {
      switch (build[i].type) {
        case "MultiPolygon": {
          ctx.beginPath()
          for (j = 0; j < build[i].geom.length; j++) {
            b = build[i].geom[j]
            if (j == 0) {
              ctx.moveTo(b[0].p1[0], b[0].p1[1])
              for (k = 1; k < b.length; k++) {
                ctx.lineTo(b[k].p1[0], b[k].p1[1])
              }
            } else {
              ctx.moveTo(b[0].p1[0], b[0].p1[1])
              for (k = b.length - 2; k >= 0; k--) {
                ctx.lineTo(b[k].p1[0], b[k].p1[1])
              }
            }
            ctx.closePath()
          }
          ctx.fill("evenodd")
          ctx.stroke()
          break
        }
        case "Point": {
          b = build[i]
          var t = b.feature.get('label')
          if (t) {
            var p = b.geom.p1
            var m = ctx.measureText(t)
            var h = Number(ctx.font.match(/\d+(\.\d+)?/g).join([]))
            ctx.fillRect(p[0] - m.width / 2 - 5, p[1] - h - 5, m.width + 10, h + 10)
            ctx.strokeRect(p[0] - m.width / 2 - 5, p[1] - h - 5, m.width + 10, h + 10)
            ctx.save()
            ctx.fillStyle = ol_color_asString(this._style.getText().getFill().getColor())
            ctx.textAlign = 'center'
            ctx.textBaseline = 'bottom'
            ctx.fillText(t, p[0], p[1])
            ctx.restore()
          }
          break
        }
        default: break
      }
    }
  }
}

export default ol_layer_Vector3D
